Skip to content

Fix/quote global rate limit double counting#196

Merged
Jagadeeshftw merged 2 commits into
AnchorNet-Org:mainfrom
MerlinTheWhiz:fix/quote-global-rate-limit-double-counting
Jul 26, 2026
Merged

Fix/quote global rate limit double counting#196
Jagadeeshftw merged 2 commits into
AnchorNet-Org:mainfrom
MerlinTheWhiz:fix/quote-global-rate-limit-double-counting

Conversation

@MerlinTheWhiz

Copy link
Copy Markdown
Contributor

Fix: Exclude quote endpoint from global rate limiter to prevent double-counting

Closes #171

Problem

In src/app.ts, the global rateLimiter() (30 req/min) is mounted at the application root before the quote-specific stricter limiter (10 req/min). Because both are independent rateLimiter() instances with separate in-memory counters, every POST /api/v1/quote request consumes budget from both buckets.
This means:

  • Quote traffic eats into the global 30/min budget, reducing capacity for other mutating routes
  • A client doing 5 quote requests + 25 requests to other mutating endpoints hits the global 429 after only 30 total requests, instead of the expected 30 non-quote requests
  • The original comment ("gets a stricter rate limit... in addition to it") was ambiguous about whether this dual-limiting was intentional

A regression test confirmed the bug: 5 quote POSTs consumed 5 global slots, leaving only 23 remaining for other routes (the 24th liquidity POST got 429, not the expected 31st).

Solution

  • Added a skipPaths option to the rateLimiter middleware. The global limiter now passes skipPaths: ["/api/v1/quote"], so quote requests bypass the global bucket entirely and are subject only to their own 10/min limit.
  • Path matching uses exact match or prefix-with-trailing-slash (path === s || path.startsWith(s + "/")) to avoid false positives on paths like /api/v1/quote-history.

Tradeoff

  • Excluding quote from the global bucket means a client doing 10 quote POSTs/min + 30 other mutating requests/min would total 40 mutating requests/min, exceeding the global limiter's 30/min budget. This is the recommended interpretation — each endpoint should enforce its own limit independently. If total-backend-write-volume capping is desired instead, skipPaths can be removed and the dual-limiter intent documented explicitly.

Changes

  • src/middleware/rateLimiter.ts — skipPaths?: string[] option with boundary-safe path matching
  • src/app.ts — Global limiter skips /api/v1/quote; comment replaced with precise behavior description and tradeoff note
  • src/routes/quote.test.ts — Regression test proving quote traffic does not consume the global mutating-request budget
  • src/middleware/rateLimiter.test.ts — Unit tests for skipPaths (skip behavior + boundary safety)

Store a SHA-256 hash of the canonicalized request body alongside cached
idempotency responses. On key reuse, compare the incoming body hash;
mismatch returns 422 IDEMPOTENCY_KEY_REUSE instead of replaying.

Uses stableStringify (sorted object keys) to avoid false-positive
rejections from semantically identical bodies with different key orderings.

Closes AnchorNet-Org#81
…e-counting

Quote requests were checked against both the global rateLimiter() (30/min)
and the quote-specific stricter limiter (10/min), causing each request to
consume budget from two independent buckets. This meant quote traffic
interfered with the global budget for other mutating routes.

Add a skipPaths option to the rate limiter so the global limiter can
exclude /api/v1/quote from its accounting. Quote requests now only
count toward their own 10/min limit.

Also fix pre-existing lint errors: add setInterval/clearInterval to
ESLint node globals and remove unused variable assignments in
settlementRepository.anchorIndex.test.ts.
@Jagadeeshftw
Jagadeeshftw merged commit 6511e69 into AnchorNet-Org:main Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants